package cz.cuni.amis.episodic.lisp.behan;

/**
 * Parser for Lisp data structures into a Java tree, functions to inspect the tree.
* @author Zdenek Behan
 */

import java.io.*;
import java.util.*;
import java.util.regex.Pattern;



class dataPrinter {
    static void printData(ArrayList<String> data, PrintWriter out) throws java.io.IOException {
        Iterator<String> iter = data.listIterator();

        if (out == null)
            return;

        out.write(iter.next());
        if (iter.hasNext()) {
            out.write('(');
            out.write(iter.next());

            while (iter.hasNext()) {
                out.write(',');
                out.write(iter.next());
            }
            out.write(')');
        }
    }
}

class WriteIndentedVisitor implements Visitor<ArrayList<String>> {

    private final int indent;
    private PrintWriter out;

    WriteIndentedVisitor(int indent, PrintWriter fout) {
        this.indent = indent;
        this.out = fout;
    }

    public Visitor<ArrayList<String>> visitTree(VisitableTree<ArrayList<String>> tree) {
        return new WriteIndentedVisitor(indent + 2, out);
    }

    public boolean visitData(VisitableTree<ArrayList<String>> parent, ArrayList<String> data, boolean isLeaf) throws java.io.IOException {
        for (int i = 0; i < indent; i++) { // TODO: naive implementation
            out.write(" ");
        }

        dataPrinter.printData(data, out);
        out.write('\n');
        return true;
    }

    public boolean postVisitData(VisitableTree<ArrayList<String>> parent, ArrayList<String> data, boolean isLeaf) throws java.io.IOException {
        return true;
    }
}

class IntoSetVisitor implements Visitor<ArrayList<String>> {
    private Set itemset;
    private PrintWriter out;

    IntoSetVisitor(Set result) {
        this.itemset = result;
    }

    public Visitor<ArrayList<String>> visitTree(VisitableTree<ArrayList<String>> tree) {
        return new IntoSetVisitor(itemset);
    }

    public boolean visitData(VisitableTree<ArrayList<String>> parent, ArrayList<String> data, boolean isLeaf) throws java.io.IOException {
        Iterator it = data.iterator();
        itemset.add(it.next());
        return true;
    }

    public boolean postVisitData(VisitableTree<ArrayList<String>> parent, ArrayList<String> data, boolean isLeaf) throws java.io.IOException {
        return true;
    }
}

public class LispTree {
    ArrayList<String> rootNode;
    private String fileName;
    private boolean hideInternal = true;

    /**
     * Constructor
     * @param aFileName File name to initialize from
     */
    public LispTree(String aFileName) {
        fileName = new String(aFileName);
        initTreeParser();
    }

    /**
     * Constructor
     * @param aFileName File name to initialize from
     * @param hideInternal true if internal goals should be hidden
     */
    public LispTree(String aFileName, boolean aHideInternal) {
        fileName = new String(aFileName);
        hideInternal = aHideInternal;
        initTreeParser();
    }

    private void initTreeParser() {
        rootNode = new ArrayList<String>();
        // Since the top-level goal is generally a hidden goal, let's add
        // one fake top-level goal of our own to keep one day events isolated
        // properly.
        rootNode.add("_TopParent");
    }

    public void walkInForest(Visitor<ArrayList<String>> visitor) {
        VisitableTree<ArrayList<String>> forest;
        Scanner input;
        try {
            input = new Scanner(new BufferedReader(new FileReader(fileName)));
            // By delimiting using an empty line, we split the file into days.
            input.useDelimiter(Pattern.compile("^$", Pattern.MULTILINE));

            boolean rv = true;
            while (rv) {
                forest = new VisitableTree<ArrayList<String>>(rootNode, true);
                rv = processDay(forest, input);
                if (rv)
                    rv &= forest.walk(visitor);
            }
            //out.flush();
        } catch (IOException e) {
            System.err.println("Error: " + e.getMessage());
        }
    }

    /**
     * Series of processing functions. They accept subtree, input string and
     * starting position as arguments, and return the first position after
     * processing locally has ended.
     */

    /**
     * Process a single input day. Input file is quite simply a sequence of
     * atoms, plus whitespace in between.
     *
     * @param node Parent of all top-level goals.
     * @param text String representation of the file.
     * @return Whether any data has been processed
     */
    private boolean processDay(VisitableTree<ArrayList<String>> node, Scanner input) {
        int newPos, curPos = 0;

        if (input.hasNext()) {
            String text = input.next();

            // Look at which comes first: closing or opening parenthesis
            newPos = findParentheses(text, curPos);

            // We should never encounter ')', as they have to be eaten by the
            // recursively called functions.
            if ((newPos == -1) || (text.charAt(newPos) == ')')) {
                return false;
                // TODO: Throw an exception
            }

            curPos = processAtom(node, text, newPos);
            return true;
        }
        return false;
    }

    /**
     * Process a tree atom (either goal or atomic action).
     *
     * @param node Parent
     * @param text Input string
     * @param pos Starting position
     * @return Position after atom is parsed
     */
    private int processAtom(VisitableTree<ArrayList<String>> node, String text, int pos) {
        assert(text.charAt(pos) == '(');

        // A goal will always start as "((", action always as "(!"
        if(text.charAt(pos+1) == '(') { // goal
            return processGoal(node, text, pos);
        } else if (text.charAt(pos+1) == '!') { // action
            return processAction(node, text, pos);
        } else { // neither - err out
            return -1; // TODO: Throw an exception
        }
    }

    /**
     * Process a goal.
     * @param node Parent
     * @param text Input string
     * @param pos Starting position
     * @return Position after goal is parsed
     */
    private int processGoal(VisitableTree<ArrayList<String>> node, String text, int pos) {
        assert(text.charAt(pos) == '(');
        assert(text.charAt(pos+1) == '(');

        int newPos, curPos = pos;

        VisitableTree<ArrayList<String>> goalNode;

        // A Syntax of a goal is:
        // ((goal descriptor) (atom1) (atom2) ... (atomn))

        // First process the goal descriptor
        String s = processAtomic(text, pos+1);
        ArrayList<String> newNode = new ArrayList<String>();
        if (hideInternal && (s.charAt(0) == '_')) {
            goalNode = node; // For internal goals, have the parent inherit our children
        } else {
            for (String str : s.split(" ")) {
                newNode.add(str);
            }
            goalNode = node.child(newNode);
        }

        curPos += s.length()+3; // add "(()"

        while (curPos != text.length()) {
            // Look at which comes first: closing or opening parenthesis
            newPos = findParentheses(text,curPos);

            if (newPos == -1)
                return text.length(); // TODO: Throw an exception

            if (text.charAt(newPos) == '(') { //
                curPos = processAtom(goalNode, text, newPos);
            } else {
                return newPos+1;
            }
        }

        return 0;
    }

    /**
     * Process an atomic action
     * @param node Parent
     * @param text Input string
     * @param pos Starting position
     * @return Position after action is parsed
     */
    private int processAction(VisitableTree<ArrayList<String>> node, String text, int pos) {
        assert(text.charAt(pos) == '(');
        assert(text.charAt(pos+1) == '!');

        String s = processAtomic(text, pos);
        ArrayList<String> newNode = new ArrayList<String>();

        // For internal actions, simply skip them
        if (!(hideInternal && (s.charAt(1) == '_'))) {
            // Skip the '!'
            for (String str : s.split(" ")) {
                newNode.add(str);
            }
            node.child(newNode);
        }

        return pos+s.length()+2; // Add the "()"
    }


///////////////////////////////////// Utility functions

    /**
     * Utility function to look for ( or ) in text.
     * @param text String to search
     * @param pos Starting position
     * @return Position of either ( or )
     */
    private static int findParentheses(String text, int pos) {
        int newPosOpen, newPosClose;

        newPosOpen = text.indexOf("(",pos);
        newPosClose = text.indexOf(")",pos);

        if (newPosClose == -1)
            newPosClose = text.length()+1;
        if (newPosOpen == -1)
            newPosOpen = text.length()+1;

        if (newPosOpen < newPosClose) { // opening first
            return newPosOpen;
        } else if (newPosOpen > newPosClose) { // closing first
            return newPosClose;
        } else { // equal = no opening or closing parenthesis
            return -1;
        }
    }

    /**
     * Utility function to process everything from a starting ( to ending )
     * @param text Input string
     * @param pos Starting position
     * @return Discovered string
     */
    private static String processAtomic(String text, int pos){
        assert(text.charAt(pos) == '(');

        int newPos = findParentheses(text, pos+1);

        if ((newPos == -1) || (text.charAt(newPos) == '('))
            return ""; // TODO: Throw an exception

        // newPos points at ')'
        return text.substring(pos+1,newPos);
    }
}
